/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package EvoAlg;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.StringTokenizer;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 *
 * @author Geost1gma
 */
public class Izhikevich extends GenericProblem {

    private final int timeconstant = 30;
    private final int I = 10;
    private final double act_threshold = 0.0;
    private final int maxTimestep = 1000;
    private double u = 0;
    private double v = -60;
    private double init_u = 0;
    private double init_v = -60;
    private double spikeThreshold = act_threshold;
    private int k_timestep = 5; // must be an odd-number
    private int currentFile = 1;
    private ArrayList<Double> izhiValues = new ArrayList<Double>();
    private double bestApproximation[] = new double[1000];
    private double max_fitness = -400.0;
    private int evaluationMethod = 0;
    private Individual bestIndi;

    @Override
    public double Evaluate(Individual Indiv) {
        throw new UnsupportedOperationException("Not supported yet.");
    }

    @Override
    public double Evaluate(Individual[] Indiv) {
        ArrayList<Double> targetRaw = null;
        try {
            targetRaw = getDat(currentFile);
        } catch (Exception ex) {
            Logger.getLogger(Izhikevich.class.getName()).log(Level.SEVERE, null, ex);
        }


        for (int i = 0; i < Indiv.length; i++) {
            //izhiValues = new ArrayList<>
            IzhikevitchAlg(Indiv[i]);
            double fit = 0;
            switch (evaluationMethod) {
                case 0:

                    double time = spikeTime(izhiValues, targetRaw);

                    if(time != -1)fit = -time;
                    else fit = -200;
                    //System.out.println(fit + " " +i);
                    break;
                case 1:
                    double interval = spikeInterval(izhiValues, targetRaw);
                    if(interval != -1)fit = -interval;
                    else fit = -200;
                    //System.out.println(fit + " " +i);
                    break;
                case 2:
                    double wave= waveform(izhiValues, targetRaw);
                    if(wave != -1)fit = -wave;
                    else fit = -200;
                    //System.out.println(fit);
                    break;
            }
            if(fit>  max_fitness){
                max_fitness = fit;
                bestIndi = Indiv[i];
//                System.out.println("nyMaxFitness" + max_fitness);
                for (int j = 0; j < bestApproximation.length; j++) {
                    bestApproximation[j] = izhiValues.get(j);

                }

            }

            Indiv[i].setFitness(fit);



            izhiValues.clear();
            u = init_u;
            v = init_v;
        }
        return 0.0;
    }

    @Override
    public void EvaluateAll(Population pop) {
        ArrayList<Double> targetRaw = null;
        try {
            targetRaw = getDat(currentFile);
        } catch (Exception ex) {
            Logger.getLogger(Izhikevich.class.getName()).log(Level.SEVERE, null, ex);
        }


        for (int i = 0; i < pop.getPopsize(); i++) {
            //izhiValues = new ArrayList<>
            IzhikevitchAlg(pop.getPop()[i]);

//            pop.getIndividualAtIndex(i).setFitness(-spikeTime(izhiValues,targetRaw));
            double fit = 0.0;
            switch (evaluationMethod) {
                case 0:

                    double time = spikeTime(izhiValues, targetRaw);
                    
                    if(time != -1)fit = -time;
                    else fit = -200;
                    //System.out.println(fit + " " +i);
                    break;
                case 1:
                    double interval = spikeInterval(izhiValues, targetRaw);
                    if(interval != -1)fit = -interval;
                    else fit = -200;
                    //System.out.println(fit + " " +i);
                    break;
                case 2:
                    double wave= waveform(izhiValues, targetRaw);
                    if(wave != -1)fit = -wave;
                    else fit = -200;
                    //System.out.println(fit + " " +i);
                    break;
            }
            //System.out.println(max_fitness + " FFFFFF ");
            if(fit>  max_fitness){
                max_fitness = fit;
                bestIndi = pop.getIndividualAtIndex(i);
                System.out.println("nyMaxFitness" + max_fitness);
                for (int j = 0; j < bestApproximation.length; j++) {
                    bestApproximation[j] = izhiValues.get(j);

                }

            }

            pop.getIndividualAtIndex(i).setFitness(fit);


            izhiValues.clear();
            u = init_u;
            v = init_v;
        }

    }

    public double v_derived(double k, double v, double u) {
        return (k * Math.pow(v, 2) + 5 * v + 140 - u + I) / timeconstant;

    }

    public double u_derived(double a, double b, double v, double u) {
        return (a * (b * v - u)) / timeconstant;
    }

    public ArrayList<Double> getDat(int id) throws NullPointerException, FileNotFoundException, IOException {
        String filename = "izzy-train" + id + ".dat";
        String location = "res/";
        return getDat(location + filename);
    }

    public ArrayList<Double> getDat(String location) throws NullPointerException, FileNotFoundException, IOException {
        File file = new File(location);
        BufferedReader br = null;
        String str = "";

        br = new BufferedReader(new FileReader(file));
        while (br.ready()) {
            str += (char) br.read();
        }
        br.close();

        ArrayList<Double> data = new ArrayList<Double>();
        StringTokenizer st = new StringTokenizer(str, " ");

        String token;
        while (st.hasMoreTokens()) {
            token = st.nextToken().trim();
            if (token != null && !token.equals("")) {
                data.add(Double.parseDouble(token));
            }
        }

        return data;
    }

    private void nextStep(Individual ind) {
        double a = ind.getGene().getGeneAtIndex(Phenotype.VALUE_A).getWeight();
        double b = ind.getGene().getGeneAtIndex(Phenotype.VALUE_B).getWeight();
        double c = ind.getGene().getGeneAtIndex(Phenotype.VALUE_C).getWeight();
        double d = ind.getGene().getGeneAtIndex(Phenotype.VALUE_D).getWeight();
        double k = ind.getGene().getGeneAtIndex(Phenotype.VALUE_K).getWeight();

        if (v > spikeThreshold) {
            v = c;
            u = u + d;
        }


        //System.out.println("a " + a + " b " + b + " c " + c  + " d " + d + " k " + k);

        u += u_derived(a, b, v, u);

        //System.out.println(u + "  U U "+ " V V " + v );

        v += v_derived(k, v, u);



    }

    public void IzhikevitchAlg(Individual ind) {
        for (int i = 0; i < 1000; i++) {
            izhiValues.add(i, v);
            nextStep(ind);
        }
    }

    public ArrayList<Integer> prepareRawData(ArrayList<Double> data) {
        double value = -1.0;
        int index = -1;
        boolean found_spike = false;
        int middle = (int) (k_timestep / 2);
        ArrayList<Integer> spikes = new ArrayList<Integer>();
        for (int i = 0; i < data.size() - k_timestep; i += k_timestep) {
            for (int j = 0; j < k_timestep; j++) {
                if (data.get(i + j) > act_threshold && data.get(i + j) > value) {
                    index = i + j;
                    value = data.get(index);
                    found_spike = true;
                }
            }
            if(found_spike) spikes.add(index);

            // reset data before we run the next window
            value = -1.0;
            found_spike = false;
        }
        return spikes;
    }

    public double spikeTime2(ArrayList<Double> rawData, ArrayList<Double> rawTarget) {
        ArrayList<Integer> data = prepareRawData(rawData);
        ArrayList<Integer> target = prepareRawData(rawTarget);

        int dataSpikes = 0;
        int targetSpikes = 0;

        for (int i = 0; i < data.size(); i++) {
            dataSpikes++;
        }
        for (int i = 0; i < target.size(); i++) {
            targetSpikes++;

        }
        int N = -1;

        if(dataSpikes >= targetSpikes && N != 0 ) N = targetSpikes;
        else                            N = dataSpikes;

        double sum = 0;

        for (int i = 0; i < N; i++) {
            sum += Math.pow(Math.abs(data.get(i)-target.get(i)),2);
        }
        sum =+ Math.abs(targetSpikes - dataSpikes);
        sum = Math.sqrt(sum);
        sum = sum/N;

        return sum;
    }

    /**
     * Spike Time Distance Metric
     * @param rawData - raw spike data
     * @param rawTarget - raw target spike data
     * @param return - returns the average length between spike trains. if no spikes found, returns -1.
     */
    public double spikeTime(ArrayList<Double> rawData, ArrayList<Double> rawTarget) {
        ArrayList<Integer> data = prepareRawData(rawData);
        ArrayList<Integer> target = prepareRawData(rawTarget);

        // get N, the minimum of the total spike counts in the two trains
        int dataSpikes = 0;
        int targetSpikes = 0;
        int N = 0;
        int p = 2; // 2 is a typical value for p. magical!


        for (int i = 0; i < data.size(); i++) {
            if (rawData.get(data.get(i)) > act_threshold) {
                dataSpikes++;
                //      System.out.println("SPIKE FOUND DATA");
            }
        }
        for (int i = 0; i < target.size(); i++) {
            if (rawTarget.get(target.get(i)) > act_threshold) {
                targetSpikes++;
            }
        }
        // set spikes to the least value
        N = (dataSpikes < targetSpikes) ? dataSpikes : targetSpikes;
        if (N == 0) {
            return -1; // we don't want to divide by zero
        }
        double sum = 0.0;
        for (int i = 0; i < N; i++) {
            sum += Math.pow(Math.abs(data.get(i) - target.get(i)), p);

        }

        //System.out.println("FDAF" + Math.sqrt(sum));
        //System.out.println(Math.pow(sum, (1 / (double) p)));

        double temp = Math.pow(sum, (1 / (double) p));
        temp += (Math.abs(dataSpikes-targetSpikes)*1000)/(2*targetSpikes);
        temp = (temp / (double) N);


        return temp;
    }

    /**
     * Spike Interval Distance Metric
     * @param rawData - raw spike data
     * @param rawTarget - raw target spike data
     * @param return - returns the average length between spike trains. if no spikes found, returns -1.
     */
    public double spikeInterval(ArrayList<Double> rawData, ArrayList<Double> rawTarget) {
        ArrayList<Integer> data = prepareRawData(rawData);
        ArrayList<Integer> target = prepareRawData(rawTarget);

        // get N, the minimum of the total spike counts in the two trains
        int dataSpikes = 0;
        int targetSpikes = 0;
        double N = 0;
        double p = 2; // 2 is a typical value for p. magical!

        for (int i = 0; i < data.size(); i++) {
            if (rawData.get(data.get(i)) > act_threshold) {
                dataSpikes++;
            }
        }
        for (int i = 0; i < target.size(); i++) {
            if (rawTarget.get(target.get(i)) > act_threshold) {
                targetSpikes++;
            }
        }
        // set spikes to the least value
        N = (dataSpikes < targetSpikes) ? dataSpikes : targetSpikes;
        if (N == 0) {
            return -1; // we don't want to divide by zero
        }
        double sum = 0.0;
        for (int i = 1; i < N; i++) {
            sum += Math.pow(Math.abs( (data.get(i) - data.get(i - 1)) - (target.get(i) - target.get(i - 1) ) ),p ) ;
        }
//        System.out.println("sum" + sum);

        sum = Math.pow(sum, (1 / (double) p));
        sum += (Math.abs(dataSpikes-targetSpikes)*1000)/(2*targetSpikes);
        
        double temp = (1 / (double) (N - 1))* sum;
        

        return temp;
    }

    /**
     * Waveform Distance Metric
     * @param rawData - The raw data NOTE: this is data which is *NOT* processed through a window
     * @param rawTarget - The raw target data to compare the data with
     */
    public double waveform(ArrayList<Double> rawData, ArrayList<Double> rawTarget) {
        // get N, the minimum of the total spike counts in the two trains
        int dataSpikes = 0;
        int targetSpikes = 0;
        int M = 0;
        double p = 2; // 2 is a typical value for p. magical!

        double timec = 2;

        M = 1000;
        double sum = 0.0;

        for (int i = 0; i < M - 1; i+=timec) {
            sum += Math.pow(Math.abs(rawData.get(i) - rawTarget.get(i)), p);
        }

        sum = Math.pow(sum, (1 / (double) p));
        //System.out.println("sum" + sum);
        return (1 / (double) M/timec) * sum;
    }

    public double[] getBestApproximation() {
        return bestApproximation;
    }

    public void setBestApproximation(double[] bestApproximation) {
        this.bestApproximation = bestApproximation;
    }

    public int getCurrentFile() {
        return currentFile;
    }

    public void setCurrentFile(int currentFile) {
        this.currentFile = currentFile;
    }

    public int getEvaluationMethod() {
        return evaluationMethod;
    }

    public void setEvaluationMethod(int evaluationMethod) {
        this.evaluationMethod = evaluationMethod;
    }

    public double getMax_fitness() {
        return max_fitness;
    }

    public void setMax_fitness(double max_fitness) {
        this.max_fitness = max_fitness;
    }

    public Individual getBestIndi() {
        return bestIndi;
    }

    public void setBestIndi(Individual bestIndi) {
        this.bestIndi = bestIndi;
    }


}
